/*
 * Tests that resource pattern matching rules work as expected.
 * @tags: [requires_replication, requires_sharding]
 */

import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";

// This test logs in users on the admin database, but doesn't log them out, which can fail with
// implicit sessions and ReplSetTest when the fixture attempts to verify data hashes at shutdown by
// authenticating as the __system user.

TestData.disableImplicitSessions = true;

function setup_users(granter) {
    const admin = granter.getSiblingDB("admin");
    assert.commandWorked(admin.runCommand({
        createUser: "admin",
        pwd: "admin",
        roles:
            ["userAdminAnyDatabase",
             "dbAdminAnyDatabase",
             "clusterAdmin",
             "readWriteAnyDatabase"]
    }));

    assert(admin.auth("admin", "admin"));
    printjson(admin.runCommand({createRole: "test_role", privileges: [], roles: []}));
    printjson(admin.runCommand({createUser: "test_user", pwd: "password", roles: ["test_role"]}));
    admin.logout();
}

function setup_dbs_and_cols(db) {
    const admin = db.getSiblingDB('admin');
    const test_db_a = db.getSiblingDB('a');
    const test_db_b = db.getSiblingDB('b');

    assert(admin.auth('admin', 'admin'));
    assert.commandWorked(test_db_a.dropDatabase({w: 'majority'}));
    assert.commandWorked(test_db_b.dropDatabase({w: 'majority'}));

    assert.commandWorked(test_db_a.createCollection("a", {writeConcern: {w: 'majority'}}));
    assert.commandWorked(test_db_a.createCollection("b", {writeConcern: {w: 'majority'}}));

    assert.commandWorked(test_db_b.createCollection("a", {writeConcern: {w: 'majority'}}));
    assert.commandWorked(test_db_b.createCollection("b", {writeConcern: {w: 'majority'}}));
    admin.logout();
}

function grant_privileges(granter, privileges) {
    const admin = granter.getSiblingDB("admin");

    assert(admin.auth("admin", "admin"));
    const result = admin.runCommand({
        grantPrivilegesToRole: "test_role",
        privileges: privileges,
        writeConcern: {w: 'majority'}
    });
    admin.logout();
    return result;
}

function revoke_privileges(granter, privileges) {
    const admin = granter.getSiblingDB("admin");

    assert(admin.auth("admin", "admin"));
    const result = admin.runCommand({
        revokePrivilegesFromRole: "test_role",
        privileges: privileges,
        writeConcern: {w: 'majority'}
    });
    admin.logout();
    return result;
}

function invalidateUserCache(verifier) {
    const admin = verifier.getSiblingDB("admin");
    assert(admin.auth('admin', 'admin'));
    assert.commandWorked(admin.runCommand("invalidateUserCache"));
    admin.logout();
}

function run_test(name, granter, verifier, privileges, collections, rst) {
    print("\n=== testing " + name + "() ===\n");

    grant_privileges(granter, privileges);
    invalidateUserCache(verifier);
    if (rst) {
        rst.awaitReplication();
    }
    const verifierDB = verifier.getSiblingDB('admin');
    assert(verifierDB.auth("test_user", "password"));

    for (var key in collections) {
        const parts = key.split(".");
        const testdb = verifier.getSiblingDB(parts[0]);
        const col = testdb.getCollection(parts[1]);

        const cb = collections[key];

        cb(testdb, col);
    }
    verifierDB.logout();

    revoke_privileges(granter, privileges);
}

function run_test_bad_resource(name, granter, resource) {
    print("\n=== testing resource fail " + name + "() ===\n");
    assert.commandFailed(grant_privileges(granter, [{resource: resource, actions: ["find"]}]));
}

function should_insert(testdb, testcol) {
    assert.doesNotThrow(function() {
        testcol.insert({a: "b"});
    });
}

function should_find(testdb, testcol) {
    assert.doesNotThrow(function() {
        testcol.findOne();
    });
}

function should_fail_find(testdb, testcol) {
    assert.throws(function() {
        testcol.findOne();
    });
}

function run_tests(granter, verifier, rst) {
    setup_users(granter);
    setup_dbs_and_cols(granter);

    run_test("specific",
             granter,
             verifier,
             [{resource: {db: "a", collection: "a"}, actions: ["find"]}],
             {
                 "a.a": should_find,
                 "a.b": should_fail_find,
                 "b.a": should_fail_find,
                 "b.b": should_fail_find
             },
             rst);

    run_test(
        "glob_collection",
        granter,
        verifier,
        [{resource: {db: "a", collection: ""}, actions: ["find"]}],
        {"a.a": should_find, "a.b": should_find, "b.a": should_fail_find, "b.b": should_fail_find},
        rst);

    run_test(
        "glob_database",
        granter,
        verifier,
        [{resource: {db: "", collection: "a"}, actions: ["find"]}],
        {"a.a": should_find, "a.b": should_fail_find, "b.a": should_find, "b.b": should_fail_find},
        rst);

    run_test("glob_all",
             granter,
             verifier,
             [{resource: {db: "", collection: ""}, actions: ["find"]}],
             {"a.a": should_find, "a.b": should_find, "b.a": should_find, "b.b": should_find},
             rst);

    run_test("any_resource",
             granter,
             verifier,
             [{resource: {anyResource: true}, actions: ["find"]}],
             {
                 "a.a": should_find,
                 "a.b": should_find,
                 "b.a": should_find,
                 "b.b": should_find,
                 "c.a": should_find
             },
             rst);

    run_test("no_global_access",
             granter,
             verifier,
             [{resource: {db: "$", collection: "cmd"}, actions: ["find"]}],
             {
                 "a.a": function(testdb, testcol) {
                     var r = testdb.stats();

                     if (r["ok"])
                         throw ("db.$.cmd shouldn't give a.stats()");
                 }
             },
             rst);

    run_test_bad_resource("empty_resource", granter, {});
    run_test_bad_resource("users_collection_any_db", granter, {collection: "users"});
    run_test_bad_resource("bad_key", granter, {myResource: "users"});
    run_test_bad_resource("extra_key", granter, {db: "test", collection: "users", cluster: true});
    run_test_bad_resource("bad_value_type", granter, {cluster: "false"});
    run_test_bad_resource("bad_collection", granter, {db: "test", collection: "$$$$"});

    run_test("mixed_find_write",
             granter,
             verifier,
             [
                 {resource: {db: "a", collection: "a"}, actions: ["find"]},
                 {resource: {db: "", collection: ""}, actions: ["insert"]}
             ],
             {
                 "a.a": function(testdb, testcol) {
                     should_insert(testdb, testcol);
                     should_find(testdb, testcol);
                 },
                 "a.b": function(testdb, testcol) {
                     should_insert(testdb, testcol);
                     should_fail_find(testdb, testcol);
                 },
                 "b.a": function(testdb, testcol) {
                     should_insert(testdb, testcol);
                     should_fail_find(testdb, testcol);
                 },
                 "b.b": function(testdb, testcol) {
                     should_insert(testdb, testcol);
                     should_fail_find(testdb, testcol);
                 },
             },
             rst);
}

const keyfile = "jstests/libs/key1";

{
    print('--- standalone node test ---');
    const conn = MongoRunner.runMongod({auth: null, keyFile: keyfile});
    run_tests(conn.getDB('test'), conn.getDB('test'));
    MongoRunner.stopMongod(conn);
    print('--- done standalone node test ---');
}

{
    print('--- replica set test ---');
    const rst =
        new ReplSetTest({name: 'testset', nodes: 2, nodeOptions: {'auth': null}, keyFile: keyfile});

    rst.startSet();
    rst.initiate();
    const primary = rst.getPrimary().getDB('admin');
    rst.awaitSecondaryNodes();
    const secondary = rst.getSecondaries()[0].getDB('admin');
    run_tests(primary, secondary, rst);
    rst.stopSet();
    print('--- done with the rs tests ---');
}

{
    print('--- sharding test ---');
    const st = new ShardingTest({
        mongos: 2,
        shard: 1,
        keyFile: keyfile,
        other: {
            mongosOptions: {'auth': null},
            configOptions: {'auth': null},
            rsOptions: {'auth': null}
        }
    });
    run_tests(st.s0.getDB('admin'), st.s1.getDB('admin'));
    st.stop();
    print('--- sharding test done ---');
}
